/**
  win32-window.c - Created by Timothy Morey on 3/7/2012.

  This file contains a Windows-based implementation of the window.h interface.

  Large portions of this file were borrowed from 
    http://nehe.gamedev.net/tutorial/creating_an_opengl_window_win32/13001/.
  The following is a message that was included in their source file:
  *		This Code Was Created By Jeff Molofee 2000
  *		A HUGE Thanks To Fredric Echols For Cleaning Up
  *		And Optimizing This Code, Making It More Flexible!
  *		If You've Found This Code Useful, Please Let Me Know.
  *		Visit My Site At nehe.gamedev.net

*/

#ifdef __WINDOWS__

#include <stdio.h>

#include <Windows.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glu.h>

#include "communication-queue.h"
#include "err.h"
#include "layer.h"
#include "logger.h"
#include "thread-utils.h"
#include "window.h"

#ifdef __WINDOWS__
#pragma warning(disable:4996)
#endif

#define MAX_WINDOWS 32
#define MAX_LAYERS 32


/*****************************************************************************
 * Private definitions
 *****************************************************************************/

typedef struct
{
  int WindowID;            /* The window handle that we give the user to 
                              identify this window */
  HWND HWnd;               /* Window handle */
  HDC HDC;                 /* Window device context */
  HGLRC HRC;               /* OpenGL rendering context */
  int Layer;
  int X;
  int Y;
  int Width;
  int Height;
  int Borderless;
} Window;

typedef struct
{
  Window* Windows[MAX_WINDOWS];
  HINSTANCE HInst;
  PIXELFORMATDESCRIPTOR PixDescriptor;
  Thread UIThread;
  CommunicationQueue Queue;
  int GlewInitialized;
  int KeepUIThreadAlive;
  HWND MessageWindow;
} GraphicsContext;

GraphicsContext g_Context;

Window* FindGLWindow(HWND handle);
LRESULT HandleWMPaint(HWND hwnd, WPARAM wparam, LPARAM lparam);
int InitUIThread();
void RunMessageLoop();
void ShutdownUIThread();
DWORD WINAPI UIThreadEntryPoint(LPVOID param);
LRESULT	CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);


/*****************************************************************************
 * Implementation of window.h functions
 *****************************************************************************/

int AddLayer(int windowID, int layer)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;
    int handled = 0;

    InitMessage(&msg, "AddLayer");
    msg.Arguments[0] = &windowID;
    msg.Arguments[1] = &layer;
    msg.Handled = &handled;
    msg.Result = &retval;
    msg.ResultSize = sizeof(int);
    Push(&g_Context.Queue, msg);
    PostMessage(g_Context.MessageWindow, WM_USER, 0, 0);

    /* Wait for the message to be handled, since we want a return value. */
    while(! handled)
      Sleep(0);
  }
  else
  {
    Window* win = 0;

    if(windowID >= 0 && windowID < MAX_WINDOWS)
      win = g_Context.Windows[windowID];
    else
      retval = NVN_EINVARGS;

    if(win)
    {
      win->Layer = layer;
      UpdateLayerTransform(win->Layer, win->Width, win->Height);
    }
  }

  return retval;
}

int CreateGLWindow(char* title, int x, int y, int width, int height, int borderless, 
                   int* window)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;
    int handled = 0;

    InitMessage(&msg, "CreateGLWindow");
    msg.Arguments[0] = &title;
    msg.Arguments[1] = &x;
    msg.Arguments[2] = &y;
    msg.Arguments[3] = &width;
    msg.Arguments[4] = &height;
    msg.Arguments[5] = &borderless;
    msg.Arguments[6] = &window;
    msg.Handled = &handled;
    msg.Result = &retval;
    msg.ResultSize = sizeof(int);
    Push(&g_Context.Queue, msg);
    PostMessage(g_Context.MessageWindow, WM_USER, 0, 0);

    /* Wait for the message to be handled, since we want a return value. */
    while(! handled)
      ThreadYield();
  }
  else
  {
    int i = 0;         /* Loop counter */
    Window* win = 0;   /* The new window */
    DWORD dwStyle;     /* Window style */
    DWORD dwExStyle;   /* Window extended style */
    RECT windowRect;
    GLuint pixFormat;

    if(! window)
      retval = NVN_EINVARGS;

    if(NVN_NOERR == retval)
    {
      win = (Window*)malloc(sizeof(Window));
      memset(win, 0, sizeof(Window));
      win->WindowID = -1;
      win->X = x;
      win->Y = y;
      win->Width = width;
      win->Height = height;
      win->Borderless = borderless;
      win->Layer = INVALID_LAYER;
      for(i = 0; i < MAX_WINDOWS; i++)
      {
        if(g_Context.Windows[i] == 0)
        {
          win->WindowID = i;
          g_Context.Windows[i] = win;
          break;
        }
      }

      if(win->WindowID < 0)
      {
        free(win);
        win = 0;
        retval = NVN_EMAXOBJS;
        WriteMessage(LogLevelError, "MAX_WINDOWS exceeded");
      }
    }

    if(NVN_NOERR == retval)
    {
      dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
      if(borderless)
        dwStyle = WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
      else
        dwStyle = WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

      windowRect.left = (long)win->X;
      windowRect.right = (long)(win->X + win->Width);
      windowRect.top = (long)win->Y;
      windowRect.bottom = (long)(win->Y + win->Height);
      AdjustWindowRectEx(&windowRect, dwStyle, FALSE, dwExStyle);
      win->X = windowRect.left;
      win->Y = windowRect.top;
      win->Width = windowRect.right - windowRect.left;
      win->Height = windowRect.bottom - windowRect.top;

      win->HWnd = CreateWindowEx(
        dwExStyle,
        "OpenGL",
        title,
        dwStyle,
        win->X,
        win->Y,
        win->Width,
        win->Height,
        NULL,             /* No Parent Window */
        NULL,             /* No Menu */
        g_Context.HInst,
        NULL              /* Dont Pass Anything To WM_CREATE */
        );

      if(! win->HWnd)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EWIN32FAIL;
        WriteMessage(LogLevelError, "CreateWindowEx failed");
      }
    }

    if(NVN_NOERR == retval)
    {
      win->HDC = GetDC(win->HWnd);

      if(! win->HDC)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EWIN32FAIL;
        WriteMessage(LogLevelError, "Failed to get device context");
      }
    }

    if(NVN_NOERR == retval)
    {
      pixFormat = ChoosePixelFormat(win->HDC, &g_Context.PixDescriptor);

      if(! pixFormat)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EWIN32FAIL;
        WriteMessage(LogLevelError, "Unable to find a matching pixel format");
      }
    }

    if(NVN_NOERR == retval)
    {
      if(! SetPixelFormat(win->HDC, pixFormat, &g_Context.PixDescriptor))
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EWIN32FAIL;
        WriteMessage(LogLevelError, "Unable to set pixel format");
      }
    }

    if(NVN_NOERR == retval)
    {
      win->HRC = wglCreateContext(win->HDC);

      if(! win->HRC)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EWGLFAIL;
        WriteMessage(LogLevelError, "Unable to create OpenGL rendering context");
      }
    }

    if(win)
    {
      if(! wglMakeCurrent(win->HDC, win->HRC))
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        WriteMessage(LogLevelError, "Unable to activate the OpenGL rendering context");
      }
    }

    if(NVN_NOERR == retval && ! g_Context.GlewInitialized)
    {
      /* Initialize GLEW so that we can use higher version OpenGL features.
         This may not be the best place to do this, but it works for now.  We
         need an initialized rendering context before we can init GLEW. */

      GLenum err = glewInit();
      if(GLEW_OK != err)
      {
        DestroyGLWindow(win->WindowID);
        win = 0;
        retval = NVN_EGLEWFAIL;
        WriteMessage(LogLevelError, "Unable to initialize GLEW");
      }

      g_Context.GlewInitialized = 1;
    }

    if(NVN_NOERR == retval)
    {
      ShowWindow(win->HWnd, SW_SHOW);
      SetForegroundWindow(win->HWnd);
      SetFocus(win->HWnd);

      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
      glClear(GL_COLOR_BUFFER_BIT);

      if(win->Layer)
        UpdateLayerTransform(win->Layer, win->Width, win->Height);

      SwapBuffers(win->HDC);
      *window = win->WindowID;
    }
  }

  return retval;
}

int DestroyGLWindow(int window)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;

    InitMessage(&msg, "DestroyGLWindow");
    msg.Arguments[0] = malloc(sizeof(int));
    msg.DestroyArgs = 1;
    *((int*)msg.Arguments[0]) = window;
    Push(&g_Context.Queue, msg);
    PostMessage(g_Context.MessageWindow, WM_USER, 0, 0);
  }
  else
  {
    Window* win = 0;

    if(window >= 0 && window < MAX_WINDOWS)
      win = g_Context.Windows[window];
    else
      retval = NVN_EINVARGS;

    if(win && win->WindowID == window)
    {
      if(win->HRC)
      {
        wglMakeCurrent(0, 0);
        wglDeleteContext(win->HRC);
        win->HRC = 0;
      }

      if(win->HDC)
      {
        ReleaseDC(win->HWnd, win->HDC);
        win->HDC = 0;
      }

      if(win->HWnd)
      {
        DestroyWindow(win->HWnd);
        win->HWnd = 0;
      }
    }
  }

  return retval;
}

int RedrawGLWindow(int window)
{
  int retval = NVN_NOERR;
  int iscur = 0;

  if(NVN_NOERR == IsCurrentThread(g_Context.UIThread, &iscur) && !iscur)
  {
    Message msg;

    InitMessage(&msg, "RedrawGLWindow");
    msg.Arguments[0] = malloc(sizeof(int));
    msg.DestroyArgs = 1;
    *((int*)msg.Arguments[0]) = window;
    Push(&g_Context.Queue, msg);
    PostMessage(g_Context.MessageWindow, WM_USER, 0, 0);
  }
  else
  {
    Window* win = 0;

    if(window >= 0 && window < MAX_WINDOWS)
      win = g_Context.Windows[window];
    else
      retval = NVN_EINVARGS;

    if(win)
    {
      if(0 == InvalidateRect(win->HWnd, 0, 0))
      {
        retval = NVN_EWIN32FAIL;
      }
    }
  }

  return retval;
}

int StartUIThread()
{
  int retval = NVN_NOERR;
  
  memset(&g_Context, 0, sizeof(GraphicsContext));
  InitQueue(&g_Context.Queue);
  g_Context.KeepUIThreadAlive = 1;

  g_Context.UIThread.Handle = 
    CreateThread(0, 0, UIThreadEntryPoint, 0, 0, &g_Context.UIThread.Id);
  if(0 == g_Context.UIThread.Handle)
    retval = NVN_ETHREADFAIL;

  // Block until the ui thread is properly initialized.
  if(NVN_NOERR == retval)
    while(g_Context.MessageWindow == 0)
      ThreadYield();

  return retval;
}

int StopUIThread()
{
  int retval = NVN_NOERR;

  if(g_Context.UIThread.Handle)
  {
    g_Context.KeepUIThreadAlive = 0;
    WaitForSingleObject(g_Context.UIThread.Handle, INFINITE);
    CloseHandle(g_Context.UIThread.Handle);
  }

  g_Context.UIThread.Handle = 0;
  g_Context.UIThread.Id = 0;

  return retval;
}


/*****************************************************************************
 * Private function implementations:
 *****************************************************************************/

Window* FindGLWindow(HWND handle)
{
  Window* retval = 0;
  int i = 0;

  if(handle)
  {
    for(i = 0; i < MAX_WINDOWS; i++)
    {
      if(g_Context.Windows[i] && 
         g_Context.Windows[i]->HWnd == handle)
      {
        retval = g_Context.Windows[i];
        break;
      }
    }
  }

  return retval;
}

LRESULT HandleWMPaint(HWND hwnd, WPARAM wparam, LPARAM lparam)
{
  int retval = 0;
  int frame = -1;
  Window* win = 0;
  PAINTSTRUCT ps;

  BeginPaint(hwnd, &ps);

  win = FindGLWindow(hwnd);
  if(win && NVN_NOERR == GetLayerCurrentFrame(win->Layer, &frame))
  {
    glClear(GL_COLOR_BUFFER_BIT);
    DrawLayer(win->Layer, frame);
    SwapBuffers(win->HDC);
  }

  EndPaint(hwnd, &ps);

  return retval;
}

int InitUIThread()
{
  int retval = NVN_NOERR;

  PIXELFORMATDESCRIPTOR pix = 
  {
		sizeof(PIXELFORMATDESCRIPTOR),  /* Size Of This Pixel Format Descriptor */
		1,                              /* Version Number */
		PFD_DRAW_TO_WINDOW |            /* Format Must Support Window */
		PFD_SUPPORT_OPENGL |            /* Format Must Support OpenGL */
		PFD_DOUBLEBUFFER,               /* Must Support Double Buffering */
		PFD_TYPE_RGBA,                  /* Request An RGBA Format */
		32,                             /* Select Our Color Depth */
		0, 0, 0, 0, 0, 0,               /* Color Bits Ignored */
		0,                              /* No Alpha Buffer */
		0,                              /* Shift Bit Ignored */
		0,                              /* No Accumulation Buffer */
		0, 0, 0, 0,                     /* Accumulation Bits Ignored */
		16,                             /* 16Bit Z-Buffer (Depth Buffer) */  
		0,                              /* No Stencil Buffer */
		0,                              /* No Auxiliary Buffer */
		PFD_MAIN_PLANE,                 /* Main Drawing Layer */
		0,                              /* Reserved */
		0, 0, 0                         /* Layer Masks Ignored */
  };

  WNDCLASS wc = 
  {
    CS_HREDRAW | CS_VREDRAW | CS_OWNDC, /* Redraw on resize, and own DC for window */
    (WNDPROC) WndProc,                  /* WndProc handles messages */
    0,                                  /* No extra window class data */
    0,                                  /* No extra window instance data */
    g_Context.HInst,
    LoadIcon(NULL, IDI_WINLOGO),        /* Load the default icon */
    LoadCursor(NULL, IDC_ARROW),        /* Load the standard arrow pointer */
    0,                                  /* No background required for OpenGL */
    0,                                  /* We don't want a menu bar */
    "OpenGL"
  };
  
  g_Context.PixDescriptor = pix;
  g_Context.HInst = GetModuleHandle(0);

  if(! RegisterClass(&wc))
  {
    retval = NVN_EWIN32FAIL;
    WriteMessage(LogLevelError, "Failed to register window class");
  }

  // Create a hidden window just so that we can receive messages
  g_Context.MessageWindow = 
    CreateWindowEx(0, "OpenGL", "", 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, 0);

  return retval;
}

void RunMessageLoop()
{
  MSG wmsg;
  Message msg;
  int valid = 0;
  int done = 0;

  while(!done && g_Context.KeepUIThreadAlive)
	{
    if(0 <= GetMessage(&wmsg, 0, 0, 0))
    {
      if(wmsg.message == WM_QUIT)
      {
        done = 1;
      }
      else if(wmsg.message == WM_USER)
      {
        // Its an internal message, so handle it here

        Pop(&g_Context.Queue, &msg, &valid);
        if(valid)
        {
          int ret = NVN_NOERR;

          if(0 == strcmp("CreateGLWindow", msg.Message))
          {
            ret = CreateGLWindow(*((char**)msg.Arguments[0]), 
                                 *((int*)msg.Arguments[1]),
                                 *((int*)msg.Arguments[2]), 
                                 *((int*)msg.Arguments[3]), 
                                 *((int*)msg.Arguments[4]),
                                 *((int*)msg.Arguments[5]), 
                                 *((int**)msg.Arguments[6]));

            if(msg.Handled)
              *msg.Handled = 1;
          }
          else if(0 == strcmp("DestroyGLWindow", msg.Message))
          {
            ret = DestroyGLWindow(*((int*)msg.Arguments[0]));

            if(msg.Handled)
              *msg.Handled = 1;
          }
          else if(0 == strcmp("AddLayer", msg.Message))
          {
            ret = AddLayer(*((int*)msg.Arguments[0]),
                           *((int*)msg.Arguments[1]));

            if(msg.Handled)
              *msg.Handled = 1;
          }
          else if(0 == strcmp("RedrawGLWindow", msg.Message))
          {
            ret = RedrawGLWindow(*((int*)msg.Arguments[0]));

            if(msg.Handled)
              *msg.Handled = 1;
          }

          if(msg.Result)
            *((int*)msg.Result) = ret;

          if(msg.DestroyArgs)
            DestroyArguments(msg);
        }
      }
      else
      {
        TranslateMessage(&wmsg);
        DispatchMessage(&wmsg);
      }
    }
	}
}

void ShutdownUIThread()
{
  UnregisterClass("OpenGL", g_Context.HInst);
}

DWORD WINAPI UIThreadEntryPoint(LPVOID param)
{
  if(NVN_NOERR == InitUIThread())
  {
    RunMessageLoop();
    ShutdownUIThread();
  }

  return 0;
}

LRESULT CALLBACK WndProc(HWND	hwnd, UINT uMsg, WPARAM wparam, LPARAM lparam)
{
  switch (uMsg)									// Check For Windows Messages
	{
    case WM_CLOSE:
    {
      PostQuitMessage(0);
      return 0;
    }
    case WM_MOVE:
    {
      Window* win = FindGLWindow(hwnd);
      if(win)
      {
        win->X = LOWORD(lparam);
        win->Y = HIWORD(lparam);
			  return 0;
      }
    }
    case WM_PAINT:
    {
      return HandleWMPaint(hwnd, wparam, lparam);
    }
    case WM_SIZE:
		{
      /* Window size has changed, so update the OpenGL window */

      Window* win = FindGLWindow(hwnd);
      if(win)
      {
        win->Width = LOWORD(lparam);
        win->Height = HIWORD(lparam);

        if(win->Layer)
        {
          UpdateLayerTransform(win->Layer, win->Width, win->Height);
          SwapBuffers(win->HDC);
        }

			  return 0;
      }
		}

		case WM_SYSCOMMAND:
		{
      /* Prevent screensaver or power saving from kicking in */
			switch (wparam)
			{
				case SC_SCREENSAVE:
        case SC_MONITORPOWER:
				return 0;
			}
			break;
		}
  }

	/* Pass All Unhandled Messages To DefWindowProc */
	return DefWindowProc(hwnd, uMsg, wparam, lparam);
}

#endif
